<template>
  <div id="3d-graph"></div>
  <Menu :content="actionContent"></Menu>
</template>
  
<script setup>
import * as THREE from "three";
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
import ForceGraph3D from "3d-force-graph";
import { computed, onMounted, reactive, watch } from "@vue/runtime-core";
import Menu from "../components/menu.vue";
import { useRouter } from "vue-router";
import axios from "axios";

const router = useRouter();
let Graph = null;
let isRotationActive = true;
let gData = null;
let cacheTargetData = null;

const actionContent = reactive([
  {
    title: "点亮",
    type: "switch",
    value: true,
    option: [true, false],
  },
  {
    title: "天空盒",
    type: "switch",
    value: true,
    option: [true, false],
  },
  {
    title: "运动",
    type: "switch",
    value: true,
    option: [true, false],
  },
  {
    title: "节点颜色",
    type: "radio",
    value: "auto",
    option: ["auto", "unity"],
  },
  {
    title: "数据源",
    type: "radio",
    value: "target",
    option: ["target", "random"],
  },
  {
    title: "节点数量",
    type: "input",
    value: 100,
  },
  {
    title: "关系数量",
    type: "input",
    value: 300,
  },
]);

const getRandomData = (N) => {
  // 方案1
  // const caheRandomData = {
  //   nodes: [...Array(N).keys()].map((i) => ({ id: i })),
  //   links: [...Array(N).keys()]
  //     .filter((id) => id)
  //     .map((id) => ({
  //       source: id,
  //       target: Math.round(Math.random() * (id - 1)),
  //     })),
  // };

  //************************************************ */
  // 方案2
  // const nodes = [...Array(N).keys()].map((i) => ({ id: i }));
  // const links = [];

  // for (let i = 1; i < nodes.length; i++) {
  //   const source = nodes[i];
  //   const numLinks = Math.round(Math.random() * 3) + 1; // 每个节点连几条边，取1-3之间的整数
  //   for (let j = i - 1; j >= Math.max(i - numLinks, 0); j--) {
  //     const target = nodes[j];
  //     links.push({ source: source.id, target: target.id });
  //   }
  // }
  //************************************************ */

  //************************************************ */
  // 方案3
  // const nodes = [...Array(N).keys()].map((i) => ({ id: i }));
  // const links = [];
  // for (let i = 1; i < N; i++) {
  //   const randomTarget = Math.floor(Math.random() * i);
  //   links.push({ source: i, target: randomTarget });
  // }
  //************************************************ */

  //************************************************ */
  // 方案4
  // 使用的是Watts-Strogatz小世界网络模型，该模型可以生成带有聚集性的随机图,让生成的图数据中的部分核心节点更聚合
  // k: 每个节点的度数。取值范围通常为10~50，数据规模越大，k可以适当增加
  // beta: 重新连接边的概率，控制聚集程度。取值范围通常为0.1~1.0，beta越小，则核心节点之间的连接越紧密，图的聚集度越高
  const nodes = [...Array(N).keys()].map((i) => ({ id: i }));
  let links = [];
  let k = 25,
    beta = 0.5;
  for (let i = 0; i < N; i++) {
    for (let j = 1; j <= k / 2; j++) {
      const neighbor = (i + j) % N;
      links.push({ source: i, target: neighbor });
    }
  }
  for (let i = 0; i < N; i++) {
    for (let j = 1; j <= k / 2; j++) {
      if (Math.random() < beta) {
        const neighbor = (i + j) % N;
        const newNeighbor = Math.floor(Math.random() * N);
        links.push({ source: i, target: newNeighbor });
        links = links.filter(
          (link) =>
            (link.source !== i || link.target !== neighbor) &&
            (link.source !== i || link.target !== newNeighbor)
        );
      }
    }
  }
  //************************************************ */

  const caheRandomData = { nodes, links };

  // debugger; // debugger才能看到
  return addNeighour(caheRandomData, "random");
};

// 节点高亮效果, 后期处理通道
const bloomPass = new UnrealBloomPass();
bloomPass.strength = 3;
bloomPass.radius = 1;
bloomPass.threshold = 0.1;

// 天空盒
const cubeTextureLoader = new THREE.CubeTextureLoader();
cubeTextureLoader.setPath("/skybox/black/");
const cubeTexture = cubeTextureLoader.load([
  "px.png",
  "nx.png",
  "py.png",
  "ny.png",
  "pz.png",
  "nz.png",
]);
cubeTexture.encoding = THREE.sRGBEncoding; //需要把色彩空间编码改一下

// 操作栏 emit
actionContent.forEach((item) => {
  watch(
    () => item,
    (val) => {
      if (val.title === "点亮") updateLight(val.value);
      else if (val.title === "天空盒") updateSkybox(val.value);
      else if (val.title === "运动") {
        isRotationActive = val.value;
        val.value ? Graph.resumeAnimation() : Graph.pauseAnimation();
      } else if (val.title === "节点颜色") updateNodeColor(val.value);
      else if (val.title === "数据源") updateData(val.value);
      else if (val.title === "节点数量") updateDataNumber(val.value);
    },
    { deep: true }
  );
});

const updateData = (val) => {
  if (val === "target") {
    gData = cacheTargetData;
    Graph.graphData(gData);
  } else if (val === "random") {
    gData = getRandomData(100, "random"); // 默认随机100个点
    Graph.graphData(gData);
  }
};

const updateDataNumber = (val) => {
  const count = val > 3000 ? 3000 : val; // 节点数量最大值
  gData = getRandomData(count);
  Graph.graphData(gData);
};

const updateNodeColor = (val) => {
  if (val === "auto") {
    // Graph.nodeAutoColorBy("user");
    console.warn("TODO: 暂不支持切回 auto");
  } else if (val === "unity") {
    Graph.nodeColor((node) =>
      highlightNodes.has(node)
        ? node === hoverNode
          ? "rgb(255,0,0,1)"
          : "rgba(255,160,0,0.8)"
        : "rgba(0,255,255,0.6)"
    );
  }
};

const updateLight = (val) => {
  if (val) {
    Graph.postProcessingComposer().addPass(bloomPass);
  } else {
    Graph.postProcessingComposer().removePass(bloomPass);
  }
};

const updateSkybox = (val) => {
  if (val) {
    Graph.scene().background = cubeTexture;
  } else {
    Graph.scene().background = null;
  }
};

// hover效果，构建邻接图
const highlightNodes = new Set();
const highlightLinks = new Set();
let hoverNode = null;

function addNeighour(data, mode) {
  data.links.forEach((link) => {
    let a, b;
    if (mode === "random") {
      a = data.nodes[link.source];
      b = data.nodes[link.target];
    } else {
      a = data.nodes.find((v) => v.id === link.source);
      b = data.nodes.find((v) => v.id === link.target);
    }

    !a.neighbors && (a.neighbors = []);
    !b.neighbors && (b.neighbors = []);
    a.neighbors.push(b);
    b.neighbors.push(a);

    !a.links && (a.links = []);
    !b.links && (b.links = []);
    a.links.push(link);
    b.links.push(link);
  });
  // console.log(data);
  return data;
}
function updateHighlight() {
  // trigger update of highlighted objects in scene
  Graph.nodeColor(Graph.nodeColor())
    .linkWidth(Graph.linkWidth())
    .linkDirectionalParticles(Graph.linkDirectionalParticles());
}

async function getGData() {
  return new Promise((resolve) => {
    axios
      .get("/json/blocks.json")
      .then((data) => {
        gData = cacheTargetData = addNeighour(data.data);
        resolve();
      })
      .catch((err) => console.error(err));
  });
}

onMounted(async () => {
  const elem = document.getElementById("3d-graph");

  await getGData();

  Graph = ForceGraph3D()(elem)
    // .jsonUrl("/json/blocks.json") // blocks
    .graphData(gData)
    .nodeAutoColorBy("user") // user
    .nodeLabel((node) => `${node.user}: ${node.description}`) // ${node.user}: ${node.description}
    // .linkCurvature(0.1)
    .linkWidth((link) => (highlightLinks.has(link) ? 4 : 1))
    .linkDirectionalParticles((link) => (highlightLinks.has(link) ? 4 : 0))
    .linkDirectionalParticleWidth(4)
    .onNodeHover((node) => {
      // no state change
      if ((!node && !highlightNodes.size) || (node && hoverNode === node))
        return;

      highlightNodes.clear();
      highlightLinks.clear();
      if (node) {
        highlightNodes.add(node);
        node.neighbors.forEach((neighbor) => highlightNodes.add(neighbor));
        node.links.forEach((link) => highlightLinks.add(link));
      }

      hoverNode = node || null;

      updateHighlight();
    })
    .linkAutoColorBy("source") // source
    // .linkOpacity(0.5)
    .onNodeRightClick((node) => {
      console.log(node);
      router.push({ path: "./child" });
    })
    .onNodeClick((node) => {
      // flyTo
      const distance = 40;
      const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);

      const newPos =
        node.x || node.y || node.z
          ? {
              x: node.x * distRatio,
              y: node.y * distRatio,
              z: node.z * distRatio,
            }
          : { x: 0, y: 0, z: distance }; // special case if node is in (0,0,0)

      Graph.cameraPosition(
        newPos, // new position
        node, // lookAt ({ x, y, z })
        3000 // ms transition duration
      );
    });

  // 屏幕自适应
  window.addEventListener("resize", onWindowResize, false);
  function onWindowResize() {
    Graph.camera().aspect = window.innerWidth / window.innerHeight;
    Graph.camera().updateProjectionMatrix();

    Graph.renderer().setSize(window.innerWidth, window.innerHeight);
  }

  // 添加天空盒
  Graph.scene().background = cubeTexture;
  //   Graph.scene().background = new THREE.Color(0xffffff);

  // 坐标轴 红绿蓝xyz
  const axes = new THREE.AxesHelper(1500);
  //   Graph.scene().add(axes);

  // 添加高亮
  Graph.postProcessingComposer().addPass(bloomPass);

  // 运动
  let angle = 0,
    distance = 2000;
  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
  // async function animate(time) {
  //   await sleep(time);
  //   console.log(1111);
  //   setInterval(() => {
  //     if (isRotationActive) {
  //       Graph.cameraPosition({
  //         x: distance * Math.sin(angle),
  //         z: distance * Math.cos(angle),
  //       });
  //       angle += Math.PI / 1000;
  //     }
  //   }, 10);
  // }
  // animate(10000);
});
</script>